package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

/**
 * Redis封装的工具类
 * 被String容器管理
 * @author IT空门_门主
 * @date 2024/4/5
 */
@Slf4j
@Component
public class RedisUtils {
    private final StringRedisTemplate stringRedisTemplate;


    public RedisUtils(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    /**
     * 创建线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * Redis保存数据
     * @param key
     * @param value
     * @param time
     * @param timeUnit
     */
    public void setRedis(String key, Object value, long time, TimeUnit timeUnit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }

    /**
     * 逻辑过期-保存
     * @param key
     * @param value
     * @param time
     * @param timeUnit
     */
    public void setRedisLogicalExpiration(String key, Object value, long time, TimeUnit timeUnit){
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));//以秒为单位
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), time, timeUnit);
    }
    /**
     * 实现缓存穿透
     */
    public <R,ID> R queryCachePenetration(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback){
        //通过Id查询商品信息
        String entries = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        R result = JSONUtil.toBean(entries, type);
        //如果没有就查询数据库
        if (StrUtil.isNotBlank(entries)){
            log.info("缓存命中，查询数据库==========查询结果:{}",result);
            return result;
        }
        //查询数据库
        R result1 =dbFallback.apply(id);

        if (result1==null){
            /**
             * todo 缓存穿透
             * 设置空值到redis中，防止缓存穿透
             * 当Id查询数据库时,该Id在数据库中不存在！！当请求量大的时候会发生缓存穿透问题
             */
            stringRedisTemplate.opsForValue().set(keyPrefix + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //保存到redis
        Map<String, Object> stringObjectMap = BeanUtil.beanToMap(result1);
        this.setRedis(keyPrefix+id, stringObjectMap,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        log.info("缓存未命中，查询数据库==========查询结果:{}",result1);
        return result1;
    }
    /**
     * 实现缓存雪崩的封装
     */
    public <T,ID> T queryCacheAvalanche(ID id,Class<T> type,String keyPrefix,Function<ID,T> dbFallback){
        //通过Id查询商品信息
        String entries = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        T typeResult = JSONUtil.toBean(entries, type);
        //如果没有就查询数据库
        if (StrUtil.isNotBlank(entries)){
            log.warn("查询的redis");
            return typeResult;
        }
        /**
         * 缓存雪崩的业务逻辑(互斥锁)
         * 只允许一个人去查询数据库
         */
        T typeResultOne = null;
        //添加互斥锁
        try {
            boolean result = addLock(LOCK_SHOP_KEY+id);
            if (!result){
                //休眠
                Thread.sleep(50);
                //没有查到就循环
                return queryCacheAvalanche(id,type,keyPrefix,dbFallback);
            }
            //查询数据库
            typeResultOne =dbFallback.apply(id);
            log.warn("从数据库中查询到商铺信息：{}",typeResultOne.toString());
            if (typeResultOne==null){
                /**
                 * todo 缓存穿透
                 * 设置空值到redis中，防止缓存穿透
                 * 当Id查询数据库时,该Id在数据库中不存在！！当请求量大的时候会发生缓存穿透问题
                 */
                stringRedisTemplate.opsForValue().set(keyPrefix + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //保存到redis
            Map<String, Object> stringObjectMap = BeanUtil.beanToMap(typeResultOne);
            stringRedisTemplate.opsForValue().set(keyPrefix + id, JSONUtil.toJsonStr(stringObjectMap), CACHE_SHOP_TTL, TimeUnit.MINUTES);
            log.warn("查询数据库");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //关闭锁
            releaseLock(LOCK_SHOP_KEY+id);
        }
        //返回结果
        return typeResultOne;
    }
    /**
     * 互斥锁解决缓存击穿
     */
    public <T,ID> T redisMutex(Class<T> type,ID id,String keyPrefix, Function<ID,T>  dbFallback){
        String result = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        if (StrUtil.isNotBlank(result)) {
            //命中:判断缓存是否过期
            RedisData redisData = JSONUtil.toBean(result, RedisData.class);
            JSONObject data = (JSONObject) redisData.getData();
            T shop1 = JSONUtil.toBean(data, type);
            log.info("命中缓存:{}", shop1);
            //判断是否过期
            if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
                return shop1;
            }
            //尝试获取互斥锁
            boolean isLock = addLock(keyPrefix + id);
            // 如果是成功
            if (isLock) {
                //1.开启新的线程(用线程池)
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    try {
                        //2.查询数据库
                        T typeResult = dbFallback.apply(id);
                        log.info("从数据库中获取数据:{}",typeResult);
                       String key =keyPrefix+id;
                        //3.将数据写进Redis
                        setRedisLogicalExpiration(key, typeResult, CACHE_SHOP_TTL,TimeUnit.MINUTES);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        //释放锁
                        releaseLock(keyPrefix + id);
                    }
                });

            }
        }
        return null;
    }
    /**
     * 添加互斥锁 (redis中的一个锁)
     */
    public boolean addLock(String  key){
        /**
         * setIfAbsent用法:当第一个的key不存在时，设置key的值为value，并返回true；当第一个key存在时，不设置key的值，返回false
         * 简单来说:当第一个人写入key成功,则下一个人在写入key会失效，除非第一个人的key失效时间到了
         */
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        /**
         * 知识点：当两个数据类型不一样的情况下，我们转换时要保证拆箱的对象不为null
         * isTrue:表示当(空和0时返回false) isFalse:表示当(空和0时返回true)
         */
        return BooleanUtil.isTrue(result);

    }
    /**
     * 释放互斥锁
     */
    public void releaseLock(String key){
        stringRedisTemplate.delete(key);
    }



}

